﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using RSSFeedReader.Data.Interop;
using RSSFeedReader.Resources;
using RSSFeedReader.Services;

namespace RSSFeedReader.Data.Models
{
    /// <summary>
    /// Class used to save and load data.
    /// </summary>
    [Serializable]
    public class ChannelDataSource
    {
        #region Members

        const string DataFileName = "feeds.xml";
        const string ApplicationFolderName = "AEA Systems";
        const string AppDataFolderName = "RSS Feed Reader";
        static readonly string AppDataSaveLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                Path.Combine(ApplicationFolderName, AppDataFolderName));

        // bool _isSyncWithIE = false;
        int _refreshPeriod = 5;
        ProxySetting _proxySetting;
        List<Channel> _channels;
        static ChannelDataSource _instance;

        readonly ILoggerService _service = Application.Current as ILoggerService;

        #endregion

        #region Constructor

        /// <summary>
        /// Returns an instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </summary>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </returns>
        static ChannelDataSource GetInstance()
        {
            // If the user does not have a data file load default feeds.
            if (!File.Exists(Path.Combine(AppDataSaveLocation, DataFileName)))
            {
                _instance = new ChannelDataSource();
                if (_instance != null)
                    _instance.Channels.AddRange(IEInterop.GetIERSSFeeds());
            }
            else
            {
                _instance = Load();

                // In case we failed to load the feeds for some reason.
                if (_instance == null)
                    _instance.Channels.AddRange(IEInterop.GetIERSSFeeds());
            }

            if (_instance._proxySetting == null)
                _instance._proxySetting = new ProxySetting();

            NetworkChange.NetworkAddressChanged += NetworkChangeNetworkAddressChanged;
            _instance.IsNetworkAvailable = NetworkInterface.GetIsNetworkAvailable();
            _instance.SyncAllChannels();
            return _instance;
        }

        static void NetworkChangeNetworkAddressChanged(object sender, EventArgs e)
        {
            _instance.IsNetworkAvailable = NetworkInterface.GetIsNetworkAvailable();
        }

        /// <summary>
        /// Initialize a new instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </summary>
        ChannelDataSource()
        {
        }

        #endregion

        #region Events

        #region ChannelLoaded
        /// <summary>
        /// Occurs when a feed has been added.
        /// </summary>
        public event EventHandler<ChannelAddedEventArgs> ChannelAdded;

        /// <summary>
        /// Fires the ChannelAdded event.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// Event arguments describing the event.
        /// </param>
        void OnChannelAdded(object sender, ChannelAddedEventArgs e)
        {
            if (ChannelAdded != null)
                ChannelAdded(sender, e);
        }
        #endregion

        #region ChannelSynchronised
        /// <summary>
        /// Occurs after a feed has been synchronised.
        /// </summary>    
        public event EventHandler<ChannelSynchronisedEventArgs> ChannelSynchronised;

        /// <summary>
        /// Fires the ChannelSynchronised event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelSynchronised(object sender, ChannelSynchronisedEventArgs e)
        {
            if (ChannelSynchronised != null)
            {
                ChannelSynchronised(sender, e);
            }
        }
        #endregion

        #region ChannelRemoved
        /// <summary>
        /// Occurs when a feed or folder is removed.
        /// </summary>
        public event EventHandler<ChannelRemovedEventArgs> ChannelRemoved;

        /// <summary>
        /// Fires the ChannelRemoved event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelRemoved(object sender, ChannelRemovedEventArgs e)
        {
            if (ChannelRemoved != null)
                ChannelRemoved(sender, e);
        }
        #endregion

        #region ChannelFolderAdded
        /// <summary>
        /// Occurs when a folder is added.
        /// </summary>
        public event EventHandler<ChannelFolderAddedEventArgs> ChannelFolderAdded;

        /// <summary>
        /// Fires the ChannelFolderAdded event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelFolderAdded(object sender, ChannelFolderAddedEventArgs e)
        {
            if (ChannelFolderAdded != null)
                ChannelFolderAdded(sender, e);
        }
        #endregion

        #region ChannelMovedToFolder
        /// <summary>
        /// Occurs when a channel/folder is moved.
        /// </summary>
        public event EventHandler<ChannelMovedToFolderEventArgs> ChannelMovedToFolder;

        /// <summary>
        /// Fires the ChannelMovedToFolder event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelMovedToFolder(object sender, ChannelMovedToFolderEventArgs e)
        {
            if (ChannelMovedToFolder != null)
                ChannelMovedToFolder(sender, e);
        }
        #endregion

        #region ChannelMoved
        /// <summary>
        /// Occurs when a channel/folder is moved.
        /// </summary>
        public event EventHandler<ChannelMovedEventArgs> ChannelMoved;

        /// <summary>
        /// Fires the ChannelMoved event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelMoved(object sender, ChannelMovedEventArgs e)
        {
            if (ChannelMoved != null)
                ChannelMoved(sender, e);
        }
        #endregion

        #endregion

        #region Public Properties

        /// <summary>
        /// Returns the instance of this class.
        /// </summary>
        public static ChannelDataSource Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = GetInstance();

                    foreach (Channel channel in _instance.Channels)
                    {
                        InitializeChildren(channel);
                    }
                }

                return _instance;
            }
        }

        /// <summary>
        /// Returns the list of Channels.
        /// </summary>
        public List<Channel> Channels
        {
            get { return _channels ?? (_channels = new List<Channel>()); }
        }

        ///// <summary>
        ///// Gets or sets a value indicating whether or not to sync feeds with Internet Explorer.
        ///// </summary>
        //public bool IsSyncWithIE
        //{
        //    get { return _isSyncWithIE; }
        //    set { _isSyncWithIE = value; }
        //}

        /// <summary>
        /// Gets or sets the refresh period for feeds.
        /// </summary>
        public int RefreshPeriod
        {
            get { return _refreshPeriod; }
            set { _refreshPeriod = value; }
        }

        /// <summary>
        /// Returns true if there is network connectivity.
        /// </summary>
        public bool IsNetworkAvailable { get; set; }

        /// <summary>
        /// Gets the root location of the applications data folder.
        /// </summary>
        public string ApplicationDataLocation
        {
            get { return AppDataSaveLocation; }
        }

        /// <summary>
        /// Gets or sets the proxy settings for the application
        /// </summary>
        public ProxySetting ProxySetting
        {
            get { return _proxySetting ?? (_proxySetting = _instance._proxySetting); }
            set { _proxySetting = value; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Add a new <see cref="RSSFeedReader.Data.Models.Channel"/> to the data.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        public void AddChannel(string url)
        {
            LoadChannelAsync(url);
        }

        /// <summary>
        /// Add a new <see cref="RSSFeedReader.Data.Models.Channel"/> to the data.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        /// <param name="parent">The parent <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        public void AddChannel(string url, Channel parent)
        {
            LoadChannelAsync(url, parent);
        }

        /// <summary>
        /// Save the current data to user app data file.
        /// </summary>
        public void Save()
        {
            if (_channels == null)
                return;

            Directory.CreateDirectory(AppDataSaveLocation);

            var xmlSer = new XmlSerializer(typeof(ChannelDataSource));
            using (Stream stream = new FileStream(Path.Combine(AppDataSaveLocation, DataFileName),
                FileMode.Create, FileAccess.Write, FileShare.None))
            {
                xmlSer.Serialize(stream, this);
            }
        }

        /// <summary>
        /// Checks if there is a <see cref="RSSFeedReader.Data.Models.Channel"/> object in 
        /// the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> instance with the specified url.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/> to look for.
        /// </param>
        /// <returns>
        /// True if a <see cref="RSSFeedReader.Data.Models.Channel"/> object is found, false otherwise.
        /// </returns>
        public bool ContainsChannel(string url)
        {
            return FindChannelByUrl(url) != null;
        }

        /// <summary>
        /// Check if the specified url string is a valid Uri.
        /// </summary>
        /// <param name="url">
        /// The string url to validate.
        /// </param>
        /// <returns>
        /// True if the url is valid, false otherwise.
        /// </returns>
        public bool IsValidUrl(string url)
        {
            string feedUrl = url;

            Uri newUri;

            if (Uri.TryCreate(feedUrl, UriKind.Absolute, out newUri))
            {
                // If the URL references a local file, but the file does not
                // exist, then the URL is considered to be invalid.
                bool isLocalFile = newUri.IsLoopback;
                if (isLocalFile && !File.Exists(newUri.LocalPath))
                    newUri = null;
            }

            return newUri != null;
        }

        /// <summary>
        /// Removes the <see cref="RSSFeedReader.Data.Models.Channel"/> with the specified url.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        public void RemoveChannel(string url)
        {
            Channel channelToRemove = FindChannelByUrl(url);
            _channels.Remove(channelToRemove);

            // Raise the event
            if (ChannelRemoved != null)
                ChannelRemoved(this, new ChannelRemovedEventArgs(channelToRemove));
        }

        /// <summary>
        /// Removes the <see cref="RSSFeedReader.Data.Models.Channel"/> with the specified ID.
        /// </summary>
        /// <param name="id">The ID of the <see cref="RSSFeedReader.Data.Models.Channel"/>.</param>
        public void RemoveChannel(Guid id)
        {
            Channel channelToRemove = FindChannelById(id);

            // Try and find the parent
            if (channelToRemove.ParentID != Guid.Empty)
            {
                Channel parent = FindChannelById(channelToRemove.ParentID);
                parent.Children.Remove(channelToRemove);
            }
            else
            {
                _channels.Remove(channelToRemove);
            }

            OnChannelRemoved(this, new ChannelRemovedEventArgs(channelToRemove));
        }

        /// <summary>
        /// Removes all the Channels.
        /// </summary>
        public void RemoveAllChannels()
        {
            _channels.Clear();
        }

        /// <summary>
        /// Synch a Channel with the specified url.
        /// </summary>
        /// <param name="id">
        /// The ID of the Channel.
        /// </param>
        public void SyncChannel(Guid id)
        {
            SyncChannelAsync(FindChannelById(id));
        }

        /// <summary>
        /// Synch all the Channels.
        /// </summary>
        public void SyncAllChannels()
        {
            _channels.ForEach(SyncChannelAsync);

            foreach (Channel channel in _channels)
            {
                SyncChannel(channel);
            }
        }

        /// <summary>
        /// Adds a new folder to the channels.
        /// </summary>
        /// <param name="title">Title of the folder.</param>
        public Channel AddNewFolder(string title)
        {
            var newFolder = new Channel(Guid.NewGuid(), title, string.Empty,
                string.Empty, string.Empty, string.Empty) { ChannelType = ChannelType.Folder };
            _channels.Add(newFolder);
            OnChannelFolderAdded(this, new ChannelFolderAddedEventArgs(newFolder));

            return newFolder;
        }

        /// <summary>
        /// Add a new folder to the channels under another folder.
        /// </summary>
        /// <param name="parentId">ID of the parent <see cref="RSSFeedReader.Data.Models.Channel"/>.</param>
        /// <param name="title">Title of the folder.</param>
        public void AddNewFolder(Guid parentId, string title)
        {
            Channel parent = FindChannelById(parentId);

            if (parent == null)
                return;

            var newFolder = new Channel(Guid.NewGuid(), parent, title, string.Empty,
                string.Empty, string.Empty, string.Empty) { ChannelType = ChannelType.Folder };
            parent.Children.Add(newFolder);
            OnChannelFolderAdded(this, new ChannelFolderAddedEventArgs(parent, newFolder));
        }

        /// <summary>
        /// Checks if the a channel with the specied ID
        /// is of the Feed ChannelType.
        /// </summary>
        /// <param name="id">The ID of the <see cref="Channel"/> to check its ChannelType.</param>
        /// <returns>True if the <see cref="Channel"/> is of the Feed ChannelType.</returns>
        public bool IsChannelTypeFeed(Guid id)
        {
            return FindChannelById(id).ChannelType == ChannelType.Feed;
        }

        /// <summary>
        /// Checks if the a channel with the specied ID
        /// is of the Folder ChannelType.
        /// </summary>
        /// <param name="id">The ID of the <see cref="Channel"/> to check its ChannelType.</param>
        /// <returns>True if the <see cref="Channel"/> is of the Folder ChannelType.</returns>
        public bool IsChannelTypeFolder(Guid id)
        {
            try
            {
                return FindChannelById(id).ChannelType == ChannelType.Folder;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Moves a channel or folder to a specified folder.
        /// </summary>
        /// <param name="channelId">The ID of the channel to move.</param>
        /// <param name="folderId">The ID of the destination folder.</param>
        public void MoveToFolder(Guid channelId, Guid folderId)
        {
            if (channelId == null)
                throw new ArgumentNullException("channelId");

            if (folderId == null)
                throw new ArgumentNullException("folderId");

            Channel channelToMove = FindChannelById(channelId);
            Channel folder = FindChannelById(folderId);

            if (channelToMove.ParentID != Guid.Empty && folder != null)
            {
                // Check folder does not all ready contain the channel
                if (folder.Children.Contains(channelToMove))
                    return;

                // Moving from folder to folder
                var parent = FindChannelById(channelToMove.ParentID);
                parent.Children.Remove(channelToMove);
                folder.Children.Add(channelToMove);
                channelToMove.ParentID = folder.ChannelID;
                OnChannelMovedToFolder(this,
                    new ChannelMovedToFolderEventArgs(channelToMove, folder, parent));
            }
            else if (folder != null)
            {
                // Check folder does not all ready contain the channel
                if (folder.Children.Contains(channelToMove))
                    return;

                // Coming from root to a folder
                _instance.Channels.Remove(channelToMove);
                folder.Children.Add(channelToMove);
                channelToMove.ParentID = folder.ChannelID;
                OnChannelMovedToFolder(this,
                    new ChannelMovedToFolderEventArgs(channelToMove, folder));
            }
            else
            {
                // Must be moving back to the root

                // Check root doesn't all ready contain the channel
                if (_instance.Channels.Contains(channelToMove))
                    return;

                Channel parent = FindChannelById(channelToMove.ParentID);
                parent.Children.Remove(channelToMove);
                channelToMove.ParentID = Guid.Empty;
                _instance.Channels.Add(channelToMove);
                ChannelMovedToFolder(this,
                    new ChannelMovedToFolderEventArgs(channelToMove));
            }
        }

        /// <summary>
        /// Moves a channel to another position before the specified toID.
        /// </summary>
        /// <param name="fromId">The ID of the channel to move.</param>
        /// <param name="toId">The ID of the channel to move before.</param>
        public void MoveChannel(Guid fromId, Guid toId)
        {
            if (fromId == null)
                throw new ArgumentNullException("fromId");

            if (toId == null)
                throw new ArgumentNullException("toId");

            List<Channel> channelsOriginal = _channels.ToList();

            try
            {
                var channelToMove = FindChannelById(fromId);
                var channelToMoveParent = FindChannelById(channelToMove.ParentID);

                if (toId == Guid.Empty)
                {
                    // Must be moving back to the root
                    channelToMoveParent.Children.Remove(channelToMove);
                    channelToMove.ParentID = Guid.Empty;
                    _instance.Channels.Add(channelToMove);
                    ChannelMoved(this, new ChannelMovedEventArgs(channelToMove));
                    return;
                }

                var channelToMoveTo = FindChannelById(toId);
                var channelToMoveToParent = FindChannelById(channelToMoveTo.ParentID);

                if (channelToMove == channelToMoveTo)
                {
                    return;
                }
                if (channelToMoveTo.ChannelType == ChannelType.Folder)
                {
                    // Moving to a folder
                    MoveToFolder(fromId, toId);
                }
                else
                {
                    int channelToMoveToIndex = -1;
                    if (channelToMoveParent != null && channelToMoveToParent != null)
                    {
                        channelToMoveToIndex = channelToMoveToParent.Children.IndexOf(channelToMoveTo);

                        if (channelToMoveParent == channelToMoveToParent)
                        {
                            // Moving from within the same folder
                            if (channelToMoveTo == channelToMoveToParent.Children.Last())
                            {
                                channelToMoveParent.Children.Remove(channelToMove);
                                channelToMoveToParent.Children.Add(channelToMove);
                            }
                            else if (channelToMoveTo == channelToMoveToParent.Children.First())
                            {
                                channelToMoveParent.Children.Remove(channelToMove);
                                channelToMoveToParent.Children.Insert(0, channelToMove);
                            }
                            else
                            {
                                channelToMoveParent.Children.Remove(channelToMove);
                                channelToMoveToParent.Children.Insert(channelToMoveToIndex, channelToMove);
                            }
                            channelToMove.ParentID = channelToMoveToParent.ChannelID;
                        }
                        else
                        {
                            // Moving from one folder to another
                            channelToMoveParent.Children.Remove(channelToMove);

                            if (channelToMoveTo == channelToMoveToParent.Children.Last())
                            {
                                channelToMoveToParent.Children.Add(channelToMove);
                            }
                            else if (channelToMoveTo == channelToMoveToParent.Children.First())
                            {
                                channelToMoveToParent.Children.Insert(0, channelToMove);
                            }
                            else
                            {
                                channelToMoveToParent.Children.Insert(channelToMoveToIndex, channelToMove);
                            }
                            channelToMove.ParentID = channelToMoveToParent.ChannelID;
                        }

                        OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                    }
                    else if (channelToMoveParent == null && channelToMoveToParent != null)
                    {
                        // Coming from root to a folder
                        channelToMoveToIndex = channelToMoveToParent.Children.IndexOf(channelToMoveTo);
                        _instance.Channels.Remove(channelToMove);

                        if (channelToMoveTo == channelToMoveToParent.Children.Last())
                        {
                            channelToMoveToParent.Children.Add(channelToMove);
                        }
                        else if (channelToMoveTo == channelToMoveToParent.Children.First())
                        {
                            channelToMoveToParent.Children.Insert(0, channelToMove);
                        }
                        else
                        {
                            channelToMoveToParent.Children.Insert(channelToMoveToIndex + 1, channelToMove);
                        }

                        channelToMove.ParentID = channelToMoveToParent.ChannelID;
                        OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                    }
                    else if (channelToMoveParent != null && channelToMoveToParent == null)
                    {
                        // Coming from folder to root at a specified feed
                        channelToMoveToIndex = _channels.IndexOf(channelToMoveTo);
                        channelToMoveParent.Children.Remove(channelToMove);

                        if (channelToMoveTo == _channels.Last())
                        {
                            _channels.Add(channelToMove);
                        }
                        else if (channelToMoveTo == _channels.First())
                        {
                            _channels.Insert(0, channelToMove);
                        }
                        else
                        {
                            _channels.Insert(channelToMoveToIndex + 1, channelToMove);
                        }

                        channelToMove.ParentID = Guid.Empty;
                        OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                    }
                    else if (channelToMoveParent == null && channelToMoveToParent == null)
                    {
                        channelToMoveToIndex = _channels.IndexOf(channelToMoveTo);

                        if (channelToMoveTo == _channels.Last())
                        {
                            _channels.Remove(channelToMove);
                            _channels.Add(channelToMove);
                        }
                        else if (channelToMoveTo == _channels.First())
                        {
                            _channels.Remove(channelToMove);
                            _channels.Insert(0, channelToMove);
                        }
                        else
                        {
                            _channels.Remove(channelToMove);
                            _channels.Insert(channelToMoveToIndex, channelToMove);
                        }

                        OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                    }
                }
            }
            catch (Exception ex)
            {
                _service.LogMessage(Strings.LogTypeWarning, ex.Message);
                // Return to original state
                _channels = channelsOriginal;
            }
        }

        /// <summary>
        /// Finds a channel with the specified ID.
        /// </summary>
        /// <param name="id">The ID of the Channel to find.</param>
        /// <returns>The Channel if found, null otherwise</returns>
        public Channel FindChannelById(Guid id)
        {
            foreach (var foundChannel in
                Channels.Select(channel => FindChannelById(channel, id).FirstOrDefault()).Where(foundChannel => foundChannel != null))
            {
                return foundChannel;
            }

            _service.LogMessage(Strings.LogTypeWarning, string.Format("No Channel with '{0}' id could be found, why?", id));
            return null;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Initialize any children in the feeds.
        /// </summary>
        /// <param name="channel">The Channel to search for children within.</param>
        static void InitializeChildren(Channel channel)
        {
            if (channel.Children.Count() <= 0) return;
            foreach (Channel child in channel.Children)
            {
                child.ParentID = channel.ChannelID;
                InitializeChildren(child);
            }
        }

        /// <summary>
        /// Loads the user settings from the data file.
        /// </summary>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </returns>
        static ChannelDataSource Load()
        {
            try
            {
                XmlSerializer xmlSer = new XmlSerializer(typeof(ChannelDataSource));
                using (Stream stream = new FileStream(Path.Combine(AppDataSaveLocation, DataFileName),
                    FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    ChannelDataSource channelDataSource = (ChannelDataSource)xmlSer.Deserialize(stream);
                    stream.Close();
                    return channelDataSource;
                }
            }
            catch (Exception ex)
            {
                ILoggerService service = Application.Current as ILoggerService;
                if (service != null)
                    service.LogMessage(Strings.LogTypeWarning, string.Format("Error deserializing file!!\n{0}", ex.Message));
            }

            return null;
        }

        /// <summary>
        /// Searches the specified channels children
        /// and syncs them.
        /// </summary>
        /// <param name="channel">The Channel to search its children.</param>
        void SyncChannel(Channel channel)
        {
            if (channel.Children.Count > 0)
            {
                foreach (Channel channelChild in channel.Children)
                    SyncChannel(channelChild);
            }
            else
            {
                if (IsChannelTypeFolder(channel.ChannelID))
                    return;

                SyncChannelAsync(channel);
            }
        }

        /// <summary>
        /// Load a channel asynchronously.
        /// </summary>
        /// <param name="url">The feed url.</param>
        /// <param name="parent">The parent folder of this channel.</param>
        async void LoadChannelAsync(string url, Channel parent = null)
        {
            if (!IsNetworkAvailable)
                return;

            try
            {
                Channel newChannel = null;
                await TaskEx.Run(() => newChannel = LoadChannel(url));

                _channels.Add(newChannel);
                OnChannelAdded(this, new ChannelAddedEventArgs(newChannel, null));

                if (parent != null)
                    ChannelDataSource.Instance.MoveChannel(newChannel.ChannelID, parent.ChannelID);
            }
            catch (Exception ex)
            {
                OnChannelAdded(this, new ChannelAddedEventArgs(ex as FeedLoadException, false, null));
            }
        }

        /// <summary>
        /// Loads the specified url feed and returns the  <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.Channel"/> class.
        /// </returns>
        /// <exception cref="RSSFeedReader.Data.Models.FeedLoadException">
        /// An exception occured while trying to parse the xml from the specified url.
        /// </exception>
        Channel LoadChannel(string url)
        {
            XElement rssXml;
            IEnumerable<Channel> channels;
            IEnumerable<Post> posts;
            Channel channel;

            try
            {
                rssXml = XElement.Load(GetXml(url));
                rssXml = StripNameSpaces(rssXml);

                channels = rssXml.Descendants("channel").Select(
                    x => new Channel(
                        Guid.NewGuid(),
                        x.Element("title").Value.Trim(),
                        x.Element("description").Value.Trim(),
                        x.Element("link").Value,
                        GetChannelImage(x),
                        url));

                channel = channels.FirstOrDefault();

                posts = rssXml.Descendants("item").Select(
                    item => new Post(
                        item.Element("title").Value.Trim(),
                        StripHtml(item.Element("description")),
                        item.Element("link").Value,
                        GetDate(item)
                        )
                        {
                            IsRead = FindReadPost(channel, item.Element("link").Value)
                        });

                if (channel == null)
                {
                    throw new Exception("Error whilst adding channel");
                }

                channel.Posts = posts.ToList();

            }
            catch (XmlException ex)
            {
                throw new FeedLoadException(string.Format(Strings.ChannelDataSource_XmlException, url),
                    url, ex);
            }
            catch (WebException ex)
            {
                throw new FeedLoadException(string.Format(Strings.ChannelDataSource_UnhandledException, url),
                    url, ex);
            }
            catch (Exception ex)
            {
                throw new FeedLoadException(string.Format(Strings.ChannelDataSource_UnhandledException, url),
                    url, ex);
            }

            return channel;
        }

        /// <summary>
        /// Gets the url of the image associated with the feed.
        /// </summary>
        /// <param name="item">
        /// The XElement item to get the image url from.
        /// </param>
        /// <returns>
        /// The url of the image.
        /// </returns>
        string GetChannelImage(XElement item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            XElement image = item.Element("image");
            if (image != null)
            {
                XElement imageUrl = image.Element("url");
                if (imageUrl != null)
                    return imageUrl.Value;
            }

            return null;
        }

        /// <summary>
        /// Sync the posts for the specified <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to sync the posts for.
        /// </param>
        async void SyncChannelAsync(Channel channel)
        {
            if (channel.ChannelType == ChannelType.Folder ||
                !IsNetworkAvailable)
                return;

            try
            {
                await TaskEx.Run(() => channel.Posts = LoadPosts(channel));
                OnChannelSynchronised(this, new ChannelSynchronisedEventArgs(channel, null));
            }
            catch (Exception ex)
            {
                OnChannelSynchronised(this, new ChannelSynchronisedEventArgs(ex as FeedSynchroniseException, false, null));
            }
        }

        /// <summary>
        /// Load the posts asynchronously for the specified <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> instance to load the posts from.
        /// </param>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.Channel"/> class.
        /// </returns>
        /// <exception cref="RSSFeedReader.Data.Models.FeedLoadException">
        /// An exception occured while trying to parse the xml associated with the specified
        /// <see cref="RSSFeedReader.Data.Models.Channel"/> instance.
        /// </exception>
        [DebuggerStepThrough]
        List<Post> LoadPosts(Channel channel)
        {
            IEnumerable<Post> posts;
            XElement rssFeed;

            try
            {
                rssFeed = XElement.Load(GetXml(channel.Url));
                rssFeed = StripNameSpaces(rssFeed);
                posts = rssFeed.Descendants("item").Select(
                    item => new Post(
                            item.Element("title").Value.Trim(),
                            StripHtml(item.Element("description")),
                            item.Element("link").Value,
                            GetDate(item)
                        )
                    {
                        IsRead = FindReadPost(channel, item.Element("link").Value)
                    });

                return posts.ToList();
            }
            catch (XmlException ex)
            {
                throw new FeedSynchroniseException(string.Format(Strings.ChannelDataSource_XmlException, channel.Url),
                    channel.Url, channel.ChannelID, ex);
            }
            catch (Exception ex)
            {
                throw new FeedSynchroniseException(string.Format(Strings.ChannelDataSource_UnhandledException, channel.Url),
                    channel.Url, channel.ChannelID, ex);
            }
        }

        /// <summary>
        /// Gets a value indicating whether or not this post has been read.
        /// </summary>
        /// <param name="channel">The <see cref="RSSFeedReader.Data.Models.Channel"/> the 
        /// <see cref="RSSFeedReader.Data.Models.Post"/> belongs to.</param>
        /// <param name="postLink">The link to the Post.</param>
        /// <returns>True if the post has been read, false otherwise.</returns>
        static bool FindReadPost(Channel channel, string postLink)
        {
            return (from post in channel.Posts where post.Link == postLink select post.IsRead).FirstOrDefault();
        }

        /// <summary>
        /// Gets the published date of the feed.
        /// </summary>
        /// <param name="item">
        /// The XElement item to get the published date from.
        /// </param>
        /// <returns>
        /// The published date string.
        /// </returns>
        static string GetDate(XElement item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            XElement pubDate = item.Element("pubDate");

            if (pubDate == null)
            {
                pubDate = item.Element("date");
            }

            return pubDate == null ? string.Empty : pubDate.Value;
        }

        /// <summary>
        /// Removes name space prefixes from the elements.
        /// </summary>
        /// <param name="xElement">
        /// The XElement to remove the namespace prefixes from.
        /// </param>
        /// <returns>
        /// An XElement without namespace prefixes.
        /// </returns>
        static XElement StripNameSpaces(XElement xElement)
        {
            string xml = xElement.ToString();
            // Remove name space declarations and characters
            xml = xml.Replace("dc:", "");
            xml = xml.Replace("slash:", "");
            xml = xml.Replace("â€“", "");
            xml = xml.Replace("â€¦", "");

            XElement element = XElement.Parse(xml);
            return element;
        }

        /// <summary>
        /// Strips the HTML parts of the description.
        /// </summary>
        string StripHtml(XElement descriptionElement)
        {
            string html = string.Empty;

            if (descriptionElement == null)
                return html;

            html = descriptionElement.Value;

            try
            {
                if (String.IsNullOrEmpty(html))
                    return String.Empty;

                // Get rid of any HTML-encoded characters, like converting &lt; to '<'
                string decodedHtml = System.Web.HttpUtility.HtmlDecode(html);

                // Remove all <open> and </close> tags.
                StringBuilder text = new StringBuilder(Regex.Replace(decodedHtml, "<[^>]*>", String.Empty));

                const string twoSpaces = "  ";
                const string oneSpace = " ";

                text.Replace(twoSpaces, oneSpace);
                text.Replace("\t", oneSpace);
                text.Replace("\n", oneSpace);

                // Limit the length of the description
                if (text.ToString().Length > 500)
                    text = new StringBuilder(text.ToString().Substring(0, 500) + "...");

                return text.ToString();
            }
            catch (Exception e)
            {
                _service.LogMessage(Strings.LogTypeWarning, string.Format("Error!! Message: {0}", e.Message));
                return html;
            }
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> in the local collection based on the url.
        /// </summary>
        /// <param name="url">
        /// The url to look for in the Lists of <see cref="RSSFeedReader.Data.Models.Channel"/>s.
        /// </param>
        /// <returns>
        /// A <see cref="RSSFeedReader.Data.Models.Channel"/> instance if found, null otherwise.
        /// </returns>
        Channel FindChannelByUrl(string url)
        {
            foreach (var foundChannel in
                Channels.Select(channel => FindChannelByUrl(channel, url).FirstOrDefault()).Where(foundChannel => foundChannel != null))
            {
                return foundChannel;
            }

            _service.LogMessage(Strings.LogTypeWarning, string.Format("No Channel with '{0}' url could be found, why?", url));
            return null;
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> instance,
        /// whose url is equal to the url passed in.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to test against
        /// the condition.
        /// </param>
        /// <param name="url">
        /// The url to compare against.
        /// </param>
        /// <returns>
        /// A IEnumerable collection of type <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </returns>
        static IEnumerable<Channel> FindChannelByUrl(Channel channel, string url)
        {
            if (channel.Url == url)
                yield return channel;

            foreach (Channel child in channel.Children)
                foreach (Channel isSameUrl in FindChannelByUrl(child, url))
                    yield return isSameUrl;
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> instance,
        /// whose id is equal to the id passed in.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to test against
        /// the condition.</param>
        /// <param name="id">The id to compare against.</param>
        /// <returns> 
        /// A IEnumerable collection of type <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </returns>
        static IEnumerable<Channel> FindChannelById(Channel channel, Guid id)
        {
            if (channel.ChannelID == id)
                yield return channel;

            foreach (Channel child in channel.Children)
                foreach (Channel foundChannel in FindChannelById(child, id))
                    yield return foundChannel;
        }

        XmlTextReader GetXml(string url)
        {
            try
            {
                WebRequest webRequest = WebRequest.Create(url);

                if (_proxySetting.IsProxyServerEnabled)
                {
                    webRequest.Proxy = new WebProxy(_proxySetting.Address, _proxySetting.Port)
                    {
                        Credentials = new NetworkCredential(_proxySetting.UserName, _proxySetting.Password, _proxySetting.Domain)
                    };
                }
                return new XmlTextReader(webRequest.GetResponse().GetResponseStream());
            }
            catch (WebException ex)
            {
                throw new FeedSynchroniseException(ex.Message, url, ex);
            }
        }

        #endregion
    }
}
